VI.FUME
今天要開始來介紹FUME這個FHIR Converter,也是系列文下半部的主要議題
在正式開始這個主題之前,我想先談談其他的Converter,
先前提到過的Firely,其釋出的SDK中就有包含了FHIR的各種Resource的物件用法,
ex:
FhirBoolean b = new() { ObjectValue = "treu" };
Patient p = new() { Contact = new() [ new Patient.ContactComponent() ], ActiveElement = b };
{
"resourceType": "Patient",
"active": "treu",
"contact": [{}]
}
關於firely SDK可以看一下他們的Documentation,如果是用C#開發的可以直接引入使用,在開發界接上會快一點
https://docs.fire.ly/projects/Firely-NET-SDK/en/latest/index.html
再來是另一個Converter,是Lorex開發的FHIR Universal Conversion Kit, 簡稱F.U.C.K <-註:請把.標好
https://github.com/Lorex/FHIR-Universal-Conversion-Kit
原則上他的邏輯會和FUME更靠近一些,以node.js開發,同樣支援REST Request與直接執行轉換,
先前沒有使用這一套轉換器的原因是在Mapping開發上會比較花時間一點,但node.js的支援性會比FUME FLASH再好一些,
另一個原因是FUME的開發界面比較容易上手,至於最終讀者想要嘗試哪個轉換器應該都能有效的輸出預期的成品。
這個專案我會認為有一個比FUME好的地方是,他在後期處理的支援比FUME還多一些,支援驗證文件與自動上傳,相較之下FUME把他弄去付費版了orz
其實應該這種產品要非常多的,事實上各大FHIR Server的開發公司,smile, Microsoft, Google等都有自己的一套FHIR Converter,
但是大多數的FHIR Converter都是針對既有格式的轉換,如CDA -> FHIR, HL7 v2 -> FHIR,
這樣子的好處是,各國的醫療院所能夠很簡單的從一個舊標準轉成一個新標準,這很好。
可是把話題繞回來,最開始我們提到了,醫療院所的醫資系統大多都是自己開發的,欄位與內容也都是自定義,本身的資料使用就不遵循HL7 v2等標準,
和國際上長期使用v2等標準不同,因此這些標準間轉換的轉換器顯然並不符合我們的需求,我們需要一個可以把散落的資料轉FHIR格式,且開發簡單快速好使用的轉換器
在面對需求的當下,筆者有嘗試過用node.js進行HardCoding,但越做越覺得開發成本巨大,而且並不能很好的覆蓋需求,
在面對多種選擇但進度毫無進展的當下,我決定採用FUME來開發,
FUME 是由以色列的醫資公司 Outburn.health開發的工具,於DevDays 2024正式發表,專案以Typescript開發
https://outburn.health/fhir-converter/
並且是開源工具,可以經由REST的方式進行轉換,
FUME由兩個部分組成 - FUME Engine與FUME Designer,後者是GUI的開發界面,可以及時回應轉換內容,
唯一可惜的是FUME Designer是需要付費的,與FUME Enterprise版本綑綁在一起,雖然是這樣說
FUME Designer提供了一個線上版本的playground可以測試撰寫的內容與轉換的結果
其實乍聽之下FUME與F.U.C.K能提供的功能非常相近,甚至後者還支援更多,那筆者為何使用FUME來開發,
FUME使用了一種混合語言做為映射結構(FUME Mapping)稱作FUME FLASH,這個FUME FLASH是將JSONata與FHIR ShortHand(FSH)合併使用的一種新語言,
FHIR的結構描述部分由FSH來處理;運算與資料處理則由JSONata來執行,
這個混合語言帶來了一個很直接的好處是,開發FUME Mapping非常直觀,這邊是一個很簡單的範例(FUME Designer預設):
Instance: $pid := $uuid()
InstanceOf: Patient
* identifier
* system = $urn
* value = 'urn:uuid:' & $pid
* identifier
* system = $exampleMrn
* value = mrn
* identifier
* system = $ssn
* value = ssn
* identifier
* system = $passportPrefix & passport_country
* value = passport_number
* active = status='active'
* name
* given = first_name
* family = last_name
* birthDate = birth_date
* gender = $translate(sex, 'gender')
* (address).address
* city = city_name
* state = state
* country = 'USA'
* line = $join([$string(house_number),street_name], ' ')
* postalCode = zip_code
* extension
* url = $extGeolocation
* extension
* url = 'latitude'
* valueDecimal = lat
* extension
* url = 'longitude'
* valueDecimal = long
* (phones).telecom
* system = 'phone'
* value = number
* use = (type='HOME'?'home':type='CELL'?'mobile')
* generalPractitioner
* identifier
* value = primary_doctor.license
* type.coding
* system = 'http://terminology.hl7.org/CodeSystem/v2-0203'
* code = 'MD'
* display = primary_doctor.full_name
* reference = $literal('Practitioner', {'identifier': primary_doctor.license})
可以看到,撰寫的方式跟閱讀IG的Profile模式是一致的,看到哪一行就寫到哪一行,非常直覺。
而輸入的部分也非常簡單直接:
{
"mrn": "PP875023983",
"status": "active",
"ssn": "123-45-6789",
"passport_number": "7429184766",
"passport_country": "USA",
"first_name": "Jessica",
"last_name": "Rabbit",
"birth_date": "1988-06-22",
"sex": "F",
"address": {
"city_name": "Orlando",
"state": "FL",
"street_name": "Buena Vista",
"house_number": 1375,
"zip_code": "3456701",
"lat": 28.3519592,
"long": -81.417283
},
"phones": [
{
"type": "HOME",
"number": "+1 (407) 8372859"
},
{
"type": "CELL",
"number": "+1 (305) 9831195"
}
],
"primary_doctor": {
"full_name": "Dr. Dolittle",
"license": "1-820958"
}
}
{
"resourceType": "Patient",
"id": "65611b12-6032-4f9a-ba02-622dd0556226",
"identifier": [
{
"system": "urn:ietf:rfc:3986",
"value": "urn:uuid:65611b12-6032-4f9a-ba02-622dd0556226"
},
{
"system": "http://this.is.an.example.uri/mrn",
"value": "PP875023983"
},
{
"system": "http://hl7.org/fhir/sid/us-ssn",
"value": "123-45-6789"
},
{
"system": "http://hl7.org/fhir/sid/passport-USA",
"value": "7429184766"
}
],
"active": true,
"name": [
{
"given": [
"Jessica"
],
"family": "Rabbit"
}
],
"birthDate": "1988-06-22",
"gender": "female",
"address": [
{
"city": "Orlando",
"state": "FL",
"country": "USA",
"line": [
"1375 Buena Vista"
],
"postalCode": "3456701",
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/geolocation",
"extension": [
{
"url": "latitude",
"valueDecimal": 28.3519592
},
{
"url": "longitude",
"valueDecimal": -81.417283
}
]
}
]
}
],
"telecom": [
{
"system": "phone",
"value": "+1 (407) 8372859",
"use": "home"
},
{
"system": "phone",
"value": "+1 (305) 9831195",
"use": "mobile"
}
],
"generalPractitioner": [
{
"identifier": {
"value": "1-820958",
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MD"
}
]
}
},
"display": "Dr. Dolittle",
"reference": "Practitioner/fume-example-doctor"
}
]
}
這樣子的轉換在Playground的響應時間不到1秒,幾乎是轉瞬完成。
關於FLASH這個語言實際上要注意的東西還真的滿不少的,很多機制是筆者在反覆撰寫實作的過程中摸索出來的,
基本上FUME是完整支援JSONata的使用方式的,所以若有無法實現出來的功能不妨閱讀一下JSONata的Documentation
https://docs.jsonata.org/overview.html
那說了這麼多好處,開發看起來也很容易很爽,那有沒有缺點呢?
有,其實也不少,大多數目前筆者遇到的麻煩大概都是沒付錢(?)上企業版,從驗證、上傳FHIR Server、快取與即時響應等都是企業版的服務範圍,
但最直接影響到的是開發便利帶來的轉換速度犧牲,以筆者目前實作事前審查IG而言,若無快取輔助在社群版(免費版)的轉換時間大約需要20-30秒,
雖然快取能讓之後的轉換時間落在1秒以內,但這樣的速度並不令人滿意,
值得注意的是,付費版似乎很好的處理掉這個問題了,以同樣的FUME Mapping在FUME Designer Playground的實測表現來說,轉換響應僅需要不到3秒,
如果大的性能差距確實令人心癢。
我會在後面的部分詳細說明這個語言個人的使用方法,但筆者也並不是完整的摸透了它,若有不足會再編輯補充。